Noughts And Crosses
Noughts And Crosses, also known as Tic-Tac-Toe, is a simple game with two players taking turns to mark the spaces in a 3-by-3 grid with X's and O's. The player who succeeds in placing 3 of their marks in a horizontal, vertical, or diagonal row is the winner - if neither of the players succeeds, then the game is drawn. This project is a basic implementation of the game with manual input from each player until the game is finished or reset to the original board.
Game Rules And Strategy
For interest, the history of Noughts And Crosses traces back to Ancient Egypt, Greece, and Rome around 1300 BC, but the first recorded reference to the name of "Noughts And Crosses" was in 1858 with a reframing to "Tic-Tac-Toe" in the 20th Century in the United States. In 1952, OXO was developed by Sandy Douglas and became one of the first known video games. When considering only the final state of the board and after taking into account board symmetries (rotations and reflections), there are 91 distinct positions won by X, 44 distinct positions won by O, and 3 distinct positions leading to a draw. The conventional rules for Noughts And Crosses are prescribed and detailed as follows:
- The game is played on a 3-by-3 grid.
- Player 1 is designated as X and Player 2 is designated as O.
- The players alternate by taking turns to mark a space with their symbol.
- The first player to place 3 of their marks in a horizontal, vertical, or diagonal row is the winner.
- If all 9 spaces are filled without a winner, the game is drawn.
Due to rotations and reflections, it should be kept in mind that every corner square, edge square, and centre square is strategically equivalent at the start, such that there are 3 distinct positions to mark during the first turn. For Player 1 to start, it is possible to win or at least force a draw from any starting square, but playing a corner square gives Player 2 the smallest choice of squares which must be played to avoid losing (although, research has shown that, if the players are not perfect, an opening move in the centre is usually more reliable). For Player 2 to start, they must always respond to a corner opening with a centre mark, respond to a centre opening with a corner mark, and respond to an edge opening with either a centre mark, corner mark next to Player 1, or edge mark opposite to Player 1 (any other responses will allow Player 1 to force the win). A perfect game (to win or at least draw) of Noughts And Crosses can then be played by choosing the first available move from the following strategy developed by Allen Newell and Herbert Simon:
- Win: If there are 2 marks in a row, place the mark to get 3 marks in a row and end the game.
- Block Win: If the opponent has 2 marks in a row, place the mark to block them from getting 3 marks in a row.
- Fork: Place the mark to cause a scenario which allows for 2 unblocked lines of 2 marks in a row.
- Block Fork: If there is only 1 possible fork for the opponent, place the mark to block this fork. Otherwise, place the mark to block all forks in any way which simultaneously allows for placing 2 marks in a row. Otherwise, place the mark to make 2 marks in a row to force the opponent into defending, as long as it does not produce a fork for the opponent.
- Centre: Place the mark in the centre.
- Opposite Corner: If the opponent has a corner mark, place the mark in the opposite corner.
- Empty Corner: Place the mark in an empty corner square on any of the four sides.
- Empty Side: Place the mark in an empty edge square on any of the four sides.
Basic Program Design
A standard example of Noughts And Crosses was developed for the project. The application consists of a message box to display information, squares of the board to mark, and reset button to restore the starting state. Each square has an event listener to wait for a player to click on the square to mark it for their turn if it is available to be marked. At the end of each turn, the squares are checked for the current player ending their turn to determine if their most recent mark has resulted in a winning combination. If a winning combination is not found, the board is check for a draw if each square has been marked. If a winning combination or draw is found at any point, the message is updated and each square is blocked from continuing until reset. Otherwise, the game continues until a winning combination or draw is found.
<button id = "message"></button> <div class = "board"> <div class = "square"></div> <div class = "square"></div> <div class = "square"></div> <div class = "square"></div> <div class = "square"></div> <div class = "square"></div> <div class = "square"></div> <div class = "square"></div> <div class = "square"></div> </div> <button id = "buttonReset">Start Over</button>
// Set variables for the corresponding elements. const squares = document.querySelectorAll(".square"); const message = document.querySelector("#message"); const buttonReset = document.querySelector("#buttonReset"); // Set variables used for the operations of the app. let currentPlayer = "X"; let gameOver = false; const winningCombinations = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; // Add an event to reset the game to the starting state. buttonReset.addEventListener("click", function () { for (const square of squares) { square.innerHTML = ""; square.className = "square"; } gameOver = false; currentPlayer = "X"; message.innerHTML = "Player " + currentPlayer + "'s Turn"; }); // Begin the game by indicating that Player X must start. message.innerHTML = "Player " + currentPlayer + "'s Turn"; // Add an event for each square of the board when the user clicks it. for (const square of squares) { square.addEventListener("click", function (event) { if (square.innerHTML !== "" || gameOver) { return; } square.innerHTML = currentPlayer; checkForWinner(); if (!gameOver) { currentPlayer = currentPlayer === "X" ? "O" : "X"; message.innerHTML = "Player " + currentPlayer + "'s Turn"; } }); } // Check if there is a winning combination on the board. function checkForWinner() { for (const combination of winningCombinations) { const [a, b, c] = combination; if ( squares[a].innerHTML === currentPlayer && squares[b].innerHTML === currentPlayer && squares[c].innerHTML === currentPlayer ) { message.innerHTML = "Player " + currentPlayer + " Wins!"; gameOver = true; for (const square of squares) { square.className = "blocked"; } break; } } checkForDraw(); } // Check if the board is full and a draw has occurred. function checkForDraw() { if (!gameOver) { let draw = true; for (const square of squares) { if (square.innerHTML === "") { draw = false; break; } } if (draw) { message.innerHTML = "Game Drawn!"; gameOver = true; for (const square of squares) { square.className = "blocked"; } } } }